import tkinter
from tkinter import ttk

from dataclasses import dataclass, field
from xml.sax.xmlreader import AttributesImpl

from typing import *
from tkx.counts import widgets as W
from tkx.counts import attrs as A
from tkx.attr_parse import parse_widget_attribute
from tkx.utils import *

False_ = False
if False_:
    from tkx.__init__ import *


# ==================== building class ========================

@dataclass
class BuildingWidget:
    """ 表示正在构建中的组件
    """
    widget: Union[tkinter.Widget, tkinter.Tk]
    place_args: dict = field(default_factory=dict)
    place_func: Union[Callable, None] = None
    children_place_args: dict = field(default_factory=dict)
    data: str = ''
    xml_tag_name: str = ''
    building_tags: Dict[str, Any] = field(default_factory=dict)
    on_place: Union[Callable[['BuildingWidget'], None], None] = None

    def __post_init__(self):
        if self.place_func is None:
            if not hasattr(self.widget, 'pack'):
                self.place_func = None
            else:
                self.place_func = self.widget.pack


# =================== 控件解析 ========================

widget_parsers: Dict[str, Callable[['TkXmlParse', Dict[str, str], 'TkxWindow'], 'BuildingWidget']] = {}


def widget_parser(name: str):
    def w(func):
        widget_parsers[name] = func
        return func

    return w


@widget_parser(W.button)
def widget_parser_button(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Button(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.menubutton)
def widget_parser_menubutton(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Menubutton(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.checkbutton)
def widget_parser_checkbutton(parser: 'TkXmlParse', attrs: Dict[str, str],
                              window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Checkbutton(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.radiobutton)
def widget_parser_radiobutton(parser: 'TkXmlParse', attrs: Dict[str, str],
                              window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Radiobutton(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.label)
def widget_parser_label(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Label(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.message)
def widget_parser_message(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        tkinter.Message(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.entry)
def widget_parser_entry(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=ttk.Entry(parser.widget_stack[-1].widget)
    )
    text = get_attrs_disposable(attrs, A.text)
    if text:
        bw.widget.insert(0, text)
    return bw


@widget_parser(W.spinbox)
def widget_parser_spinbox(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=ttk.Spinbox(parser.widget_stack[-1].widget)
    )
    value = get_attrs_disposable(attrs, A.value)
    if value:
        bw.widget.set(value)
    return bw


@widget_parser(W.combobox)
def widget_parser_combobox(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=ttk.Combobox(parser.widget_stack[-1].widget)
    )
    bw.building_tags['list'] = []
    text = get_attrs_disposable(attrs, A.text)
    if text:
        bw.widget.set(text)

    return bw


@widget_parser(W.optionmenu)
def widget_parser_optionmenu(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    v = parse_var(window, get_attrs_disposable(attrs, A.valuevar))
    bw = BuildingWidget(
        widget=ttk.OptionMenu(parser.widget_stack[-1].widget, variable=v)
    )
    bw.building_tags['list'] = []
    bw.building_tags['value'] = get_attrs_disposable(attrs, [A.text, A.value])
    bw.widget.set_menu(bw.building_tags['value'], *bw.building_tags['list'])
    return bw


@widget_parser(W.option)
def widget_parser_option(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    if last.xml_tag_name == W.combobox:
        text = get_attrs_disposable(attrs, A.text, 'option')
        last.building_tags['list'].append(text)
        last.widget['values'] = last.building_tags['list']
        selected = get_attrs_disposable(attrs, A.default)
        if selected:
            last.widget.set(text)
    elif last.xml_tag_name == W.optionmenu:
        text = get_attrs_disposable(attrs, A.text, 'option')
        last.building_tags['list'].append(text)
        if get_attrs_disposable(attrs, A.default):
            last.building_tags['value'] = text
        last.widget.set_menu(last.building_tags['value'], *last.building_tags['list'])
    else:
        raise Exception(f"{W.option} 只能用于 {W.combobox} 或 {W.option} 中，不能用在 {last.xml_tag_name} 中。")
    return None


@widget_parser(W.text)
def widget_parser_text(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=tkinter.Text(parser.widget_stack[-1].widget)
    )
    text = get_attrs_disposable(attrs, A.text)
    if text:
        bw.widget.insert(0, text)
    return bw


@widget_parser(W.separator)
def widget_parser_separator(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=ttk.Separator(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.scale)
def widget_parser_scale(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        widget=ttk.Scale(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.scrollbar)
def widget_parser_scale(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    bw = BuildingWidget(
        widget=ttk.Scrollbar(last.widget)
    )
    orient = get_attrs_disposable(attrs, A.orient, 'vertical')
    bw.widget['orient'] = orient
    target = get_attrs_disposable(attrs, A.target)
    if target:
        target = window.widgets[target]
        if orient == 'vertical':
            target['yscrollcommand'] = bw.widget.set
            bw.widget['command'] = target.yview
        elif orient == 'horizontal':
            target['xscrollcommand'] = bw.widget.set
            bw.widget['command'] = target.xview
    return bw


@widget_parser(W.listbox)
def widget_parser_listbox(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        tkinter.Listbox(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.listitem)
def widget_parser_listitem(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    if last.xml_tag_name != W.listbox:
        raise Exception(f"{W.listitem} 只能给 {W.listbox} 使用。")
    text = get_attrs_disposable(attrs, A.text, 'item')
    last.widget.insert('end', text)
    return None


@widget_parser(W.box)
def widget_parser_box(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    text = get_attrs_disposable(attrs, A.text, None)
    if isinstance(text, str):
        bw = BuildingWidget(ttk.Labelframe(parser.widget_stack[-1].widget, text=text))
    else:
        bw = BuildingWidget(ttk.Frame(parser.widget_stack[-1].widget))
    return bw


@widget_parser(W.row)
def widget_parser_row(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = widget_parser_box(parser, attrs, window)
    bw.children_place_args['side'] = 'left'
    return bw


@widget_parser(W.column)
def widget_parser_column(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = widget_parser_box(parser, attrs, window)
    bw.children_place_args['side'] = 'top'
    return bw


@widget_parser(W.canvas)
def widget_parser_canvas(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        tkinter.Canvas(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.treeview)
def widget_parser_treeview(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Treeview(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.progressbar)
def widget_parser_progressbar(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Progressbar(parser.widget_stack[-1].widget)
    )

    return bw


@widget_parser(W.notebook)
def widget_parser_notebook(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        ttk.Notebook(parser.widget_stack[-1].widget)
    )
    return bw


@widget_parser(W.tab)
def widget_parser_tab(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    if last.xml_tag_name != W.notebook:
        raise Exception(f"{W.tab} 只能给 {W.notebook} 使用。")
    text = get_attrs_disposable(attrs, A.text, 'tab')
    select = get_attrs_disposable(attrs, A.default, None)
    bw = widget_parser_box(parser, attrs, window)

    last.widget.add(bw.widget, text=text)
    if select:
        last.widget.select(bw.widget)

    bw.place_func = None
    return bw


@widget_parser(W.panedwindow)
def widget_parser_panedwindow(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    orient = get_attrs_disposable(attrs, A.orient)
    bw = BuildingWidget(
        ttk.Panedwindow(parser.widget_stack[-1].widget, orient=orient)
    )
    return bw


@widget_parser(W.paned)
def widget_parser_paned(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    if last.xml_tag_name != W.panedwindow:
        raise Exception(f"{W.paned} 只能给 {W.panedwindow} 使用。")
    bw = widget_parser_box(parser, attrs, window)
    last.widget.add(bw.widget, weight=get_attrs_disposable(attrs, A.weight))
    bw.place_func = None
    return bw


@widget_parser(W.window)
def widget_parser_window(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(
        tkinter.Tk()
    )
    geo = get_attrs_disposable(attrs, A.geometry, '400x300')
    if geo.endswith('+center'):
        geo = geo[:-7]
        x, y = list(map(int, geo.split('x')))
        w = bw.widget.winfo_screenwidth()
        h = bw.widget.winfo_screenheight()
        geo = f'{x}x{y}+{int((w - x) / 2)}+{int((h - y) / 2)}'
    bw.widget.geometry(geo)

    title = get_attrs_disposable(attrs, 'title text')
    bw.widget.title(title)
    icon = get_attrs_disposable(attrs, 'icon')
    bw.widget.iconbitmap(icon)
    orr = get_attrs_disposable(attrs, 'overrideredirect')
    bw.widget.overrideredirect(bool(orr))

    for k in list(attrs.keys()):
        try:
            bw.widget.attributes(f'-{k}', attrs[k])
        except Exception as e:
            bw.widget[k] = attrs[k]

        del attrs[k]

    return bw


@widget_parser(W.menubar)
def widget_parser_menubar(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]
    if last.xml_tag_name != W.window:
        raise Exception(f"{W.menubar} 组件必须直接放在 {W.window} 标签内的最外层。")
    bw = BuildingWidget(
        tkinter.Menu(last.widget, tearoff=0)
    )
    last.widget.config(menu=bw.widget)
    bw.place_func = None
    return bw


@widget_parser(W.menu)
def widget_parser_menu(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    last = parser.widget_stack[-1]

    if last.xml_tag_name not in [W.menubar, W.menu, W.menubutton]:
        raise Exception(f"{W.menu} 组件只能放在 {W.menu} 或 {W.menubar} 或 {W.menubutton} 中。")

    bw = BuildingWidget(
        tkinter.Menu(last.widget, tearoff=0)
    )
    bw.place_func = None

    text = get_attrs_disposable(attrs, [A.text, A.label], 'menu')
    if last.xml_tag_name == W.menubutton:
        last.widget['menu'] = bw.widget
    else:
        last.widget.add_cascade(label=text, menu=bw.widget)

    return bw


def _widget_parser_menu_item(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow', name: str) -> dict:
    last = parser.widget_stack[-1]

    if last.xml_tag_name not in [W.menubar, W.menu, W.menubutton]:
        raise Exception(f"{name} 组件只能放在 {W.menu} 或 {W.menubar} 或 {W.menubutton} 中。")

    bw = BuildingWidget(None)
    bw.place_func = None

    text = get_attrs_disposable(attrs, [A.text, A.label], 'menu')
    command = get_attrs_disposable(attrs, A.command)
    if command is None:
        command = None
    else:
        command = get_command(window, command)
    res = {'label': text, 'command': command}
    valuevar = get_attrs_disposable(attrs, 'valuevar')
    if valuevar:
        res['variable'] = parse_var(window, valuevar)
    return res


@widget_parser(W.menu_command)
def widget_parser_menu_command(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    a = _widget_parser_menu_item(parser, attrs, window, W.menu_command)
    parser.widget_stack[-1].widget.add_cascade(**a, **attrs)
    return None


@widget_parser(W.menu_checkbutton)
def widget_parser_menu_checkbutton(parser: 'TkXmlParse', attrs: Dict[str, str],
                                   window: 'TkxWindow') -> 'BuildingWidget':
    a = _widget_parser_menu_item(parser, attrs, window, W.menu_command)
    parser.widget_stack[-1].widget.add_checkbutton(**a, **attrs)
    return None


@widget_parser(W.menu_radiobutton)
def widget_parser_menu_radiobutton(parser: 'TkXmlParse', attrs: Dict[str, str],
                                   window: 'TkxWindow') -> 'BuildingWidget':
    a = _widget_parser_menu_item(parser, attrs, window, W.menu_command)
    parser.widget_stack[-1].widget.add_radiobutton(**a, **attrs)
    return None


@widget_parser(W.menu_separator)
def widget_parser_menu_separator(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    _widget_parser_menu_item(parser, attrs, window, W.menu_command)
    parser.widget_stack[-1].widget.add_separator()
    return None


@widget_parser(W.style)
def widget_parser_style(parser: 'TkXmlParse', attrs: Dict[str, str], window: 'TkxWindow') -> 'BuildingWidget':
    bw = BuildingWidget(None)
    bw.place_func = None
    return bw


# ================= parse func =====================


def parse_xml_tag(name: str, parser: 'TkXmlParse', attrs: AttributesImpl, window: 'TkxWindow') \
        -> Union['BuildingWidget', None]:
    """ 解析一个 xml 标签，返回一个 buildingWidget 或空。

    某些标签不会产生 buildingWidget，比如 option。
    某些标签产生的 buildingWidget 中的 widget 为 None，比如 style。
    """
    if name not in widget_parsers:
        raise Exception(f"不存在的组件：{name}")

    attrs = dict(attrs)

    bw = widget_parsers[name](parser, attrs, window)
    if bw:
        bw.xml_tag_name = name
        parse_widget_attribute(bw, attrs, window)
        return bw
    return None


def parse_characters(content: str, parser: 'TkXmlParse', window: 'TkxWindow') -> None:
    """ 解析 xml 中的字符文本。

    注意大部分的组件标签不支持内嵌文本。目前只有 style 标签支持。
    """
    last = parser.widget_stack[-1]
    if last.xml_tag_name == W.style:
        lines = content.split('\n')
        for line in lines:
            line = line.strip()
            if not line:
                continue
            if line.startswith('[') and line.endswith(']'):
                target_style = line[1:-1].strip()
                parser.style_parser.switch(target_style)
                continue
            k, v = line.split(':')
            parser.style_parser.add_style(k.strip(), v.strip())
    else:
        raise Exception(f"{last.xml_tag_name} 不支持添加文字内容。")
